/**
 * @copyright (c) 2024, MacRsh
 *
 * @license SPDX-License-Identifier: Apache-2.0
 *
 * @date 2024-08-03    MacRsh       First version
 */

#include <hashmap.h>

static int hashmap_resize(HashMap_t *map, size_t capacity) {
    HashNode_t **new_buckets;
    HashNode_t *node, *prev;
    size_t index, i;

    /* Allocate new buckets */
    capacity = (capacity != 0) ? capacity : 1;
    new_buckets = calloc(capacity, sizeof(HashNode_t *));
    if (!new_buckets) {
        return -ENOMEM;
    }

    /* Rehash the hashmap */
    for (i = 0; i < map->capacity; i++) {
        node = map->buckets[i];
        while (node) {
            prev = node;
            node = node->next;
            index = prev->key % capacity;
            prev->next = new_buckets[index];
            new_buckets[index] = prev;
        }
    }

    /* Update the hashmap */
    free(map->buckets);
    map->buckets = new_buckets;
    map->capacity = capacity;
    return 0;
}

/**
 * @brief This function initializes a hashmap.
 *
 * @param map The hashmap to be initialized.
 * @param capacity The initial size of the hashmap.
 * @param load_factor The load factor of the hashmap.
 *
 * @return 0 on success, otherwise an error code.
 */
int hashmap_init(HashMap_t *map, size_t capacity, float load_factor) {
    if ((!map) || (capacity == 0) || (load_factor <= 0) || (load_factor >= 1)) {
        return -EINVAL;
    }

    /* Initialize hashmap buckets */
    map->buckets = calloc(capacity, sizeof(HashNode_t *));
    if (!map->buckets) {
        return -ENOMEM;
    }
    map->capacity = capacity;
    map->count = 0;
    map->load_factor = load_factor;
    map->shrink_factor = (1 - load_factor) / 2;
    return 0;
}

/**
 * @brief This function deinitializes a hashmap.
 *
 * @param map The hashmap to be deinitialized.
 */
void hashmap_deinit(HashMap_t *map) {
    if (!map) {
        return;
    }

    /* Deinitialize hashmap */
    hashmap_clear(map);
    free(map->buckets);
    map->buckets = NULL;
    map->capacity = 0;
}

/**
 * @brief This function creates a hashmap.
 *
 * @param capacity The initial size of the hashmap.
 * @param load_factor The load factor of the hashmap.
 * @return The created hashmap on success, otherwise NULL.
 */
HashMap_t *hashmap_new(size_t capacity, float load_factor) {
    HashMap_t *map;

    /* Create hashmap */
    map = malloc(sizeof(HashMap_t));
    if (!map) {
        return NULL;
    }
    if (hashmap_init(map, capacity, load_factor)) {
        free(map);
        return NULL;
    }
    return map;
}

/**
 * @brief This function destroys a hashmap.
 *
 * @param map The hashmap to be destroyed.
 */
void hashmap_del(HashMap_t *map) {
    if (!map) {
        return;
    }

    /* Destroy hashmap */
    hashmap_deinit(map);
    free(map);
}

/**
 * @brief This function inserts a node into a hashmap.
 *
 * @param map The hashmap to insert the node into.
 * @param key The key of the node.
 * @param value The value of the node.
 * @return 0 on success, otherwise an error code.
 */
int hashmap_insert(HashMap_t *map, uint32_t key, void *value) {
    HashNode_t *node;
    size_t index;

    if ((!map) || (!map->capacity)) {
        return -EINVAL;
    }

    /* Resize hashmap if load factor is too high */
    if (((float)(map->count / map->capacity)) > map->load_factor) {
        hashmap_resize(map, map->capacity * 2);
    }

    /* Insert node into hashmap */
    index = key % map->capacity;
    node = map->buckets[index];
    while (node) {
        if (node->key != key) {
            node = node->next;
            continue;
        }

        /* Update node if key already exists */
        node->value = value;
        return 0;
    }

    /* Create new node */
    node = malloc(sizeof(HashNode_t));
    if (!node) {
        return -ENOMEM;
    }
    node->key = key;
    node->value = value;
    node->next = map->buckets[index];
    map->buckets[index] = node;
    map->count++;
    return 0;
}

/**
 * @brief This function removes a node from a hashmap.
 *
 * @param map The hashmap to remove the node from.
 * @param key The key of the node.
 * @return The value of the node on success, otherwise NULL.
 */
void *hashmap_remove(HashMap_t *map, uint32_t key) {
    HashNode_t *node, *prev;
    void *value;
    size_t index;

    if ((!map) || (!map->capacity)) {
        return NULL;
    }

    /* Remove node from hashmap */
    index = key % map->capacity;
    node = map->buckets[index];
    prev = NULL;
    while (node) {
        if (node->key != key) {
            prev = node;
            node = node->next;
            continue;
        }

        /* Update hashmap */
        if (!prev) {
            map->buckets[index] = node->next;
        } else {
            prev->next = node->next;
        }
        value = node->value;
        free(node);
        map->count--;

        /* Resize hashmap if load factor is too low */
        if (((float)map->count / map->capacity) < map->shrink_factor) {
            hashmap_resize(map, map->capacity / 2);
        }
        return value;
    }
    return NULL;
}

/**
 * @brief This function clears a hashmap.
 *
 * @param map The hashmap to clear.
 */
void hashmap_clear(HashMap_t *map) {
    HashNode_t *node, *prev;
    size_t i;

    if (!map) {
        return;
    }

    /* Free all nodes in hashmap */
    for (i = 0; i < map->capacity; i++) {
        node = map->buckets[i];
        while (node) {
            prev = node;
            node = node->next;
            free(prev);
        }
        map->buckets[i] = NULL;
    }
    map->count = 0;
}

/**
 * @brief This function gets a node from a hashmap.
 *
 * @param map The hashmap to get the node from.
 * @param key The key of the node.
 * @return The value of the node on success, otherwise NULL.
 */
void *hashmap_get(HashMap_t *map, uint32_t key) {
    HashNode_t *node;
    size_t index;

    if ((!map) || (!map->capacity)) {
        return NULL;
    }

    /* Find node in hashmap */
    index = key % map->capacity;
    node = map->buckets[index];
    while (node) {
        if (node->key != key) {
            node = node->next;
            continue;
        }
        return node->value;
    }
    return NULL;
}

/**
 * @brief This function returns the number of elements in a hashmap.
 *
 * @param map The hashmap to get the length of.
 * @return The number of elements in the hashmap.
 */
size_t hashmap_len(HashMap_t *map) {
    if (!map) {
        return 0;
    }

    /* Return number of elements in hashmap */
    return map->count;
}

/**
 * @brief This function checks if a hashmap is empty.
 *
 * @param map The hashmap to check.
 * @return true if the hashmap is empty, false otherwise.
 */
bool hashmap_is_empty(HashMap_t *map) {
    if (!map) {
        return true;
    }

    /* Return true if hashmap is empty */
    return (map->count == 0);
}

/**
 * @brief This function returns the capacity of a hashmap.
 *
 * @param map The hashmap to get the capacity of.
 * @return The capacity of the hashmap.
 */
size_t hashmap_capacity(HashMap_t *map) {
    if (!map) {
        return 0;
    }

    /* Return capacity of hashmap */
    return map->capacity;
}

/**
 * @brief This function reserves space in a hashmap.
 *
 * @param map The hashmap to reserve space in.
 * @param reserve_size The amount of space to reserve.
 * @return 0 on success, otherwise an error code.
 */
int hashmap_reserve(HashMap_t *map, size_t reserve_size) {
    if (!map) {
        return -EINVAL;
    }
    if (reserve_size <= (map->capacity - map->count)) {
        return 0;
    }

    /* Resize hashmap to reserve space */
    return hashmap_resize(map, reserve_size + map->capacity);
}

/**
 * @brief This function shinks a hashmap to a new size.
 *
 * @param map The hashmap to shrink.
 * @param capacity The new size of the hashmap.
 * @return 0 on success, otherwise an error code.
 */
int hashmap_shrink_to(HashMap_t *map, size_t capacity) {
    if ((!map) || (capacity > map->capacity) || (capacity < map->count)) {
        return -EINVAL;
    }
    if (capacity == map->capacity) {
        return 0;
    }

    /* Shrink hashmap to new size */
    return hashmap_resize(map, capacity);
}

/**
 * @brief This function shrinks a hashmap to fit its current number of elements.
 *
 * @param map The hashmap to shrink.
 * @return 0 on success, otherwise an error code.
 */
int hashmap_shrink_to_fit(HashMap_t *map) {
    if ((!map) || (!map->capacity)) {
        return -EINVAL;
    }

    /* Shrink hashmap to fit current number of elements */
    return hashmap_resize(map, map->count);
}

/**
 * @brief This function checks if a hashmap contains a key.
 *
 * @param map The hashmap to check.
 * @param key The key to check.
 * @return true if the hashmap contains the key, false otherwise.
 */
bool hashmap_contains_key(HashMap_t *map, uint32_t key) {
    return (hashmap_get(map, key) != NULL);
}

/**
 * @brief This function calculates the hash value of a string.
 *
 * @param str The string to calculate the hash value.
 * @return The hash value of the string.
 */
uint32_t hash_string(const char *str) {
    uint32_t hash;
    int c;

    /* djb2 hash function */
    for (hash = 5381; (c = *str); str++) {
        hash = ((hash << 5) + hash) ^ c;
    }
    return hash;
}
